Rust実装でPiping Severの転送速度を1.7倍~1.8倍高速化 (Hyper)
#piping-server-rust #Rust #Piping_Server #Hyper #制作物
GitHubリポジトリ
https://gh-card.dev/repos/nwtgck/piping-server-rust.svg https://github.com/nwtgck/piping-server-rust
Rustで実装する理由
なぜPiping ServerをRustで実装したいか?
Node.js(TypeScript) vs Rustの転送速度の比較
単純な転送速度の比較。
localhostに立てたPiping Serverに /dev/zeroを無限に送信して、/dev/nullに無限に捨ててその時の転送速度をcurlの表示で見る感じ。
https://youtu.be/KRe_5L6YijE
Rust版で1.6GB/秒で転送できていて、だいたいオリジナルのTypeScript(Node.js)の1.7~1.8倍の転送速度になっている。
目視でわかる速さの向上でRustの凄さがわかる。メモリ効率など調べれば他の点でも良いところが分かるかもしれない。
サーバーの動かし方
2種類の方法で立て方を紹介。
Docker
Dockerですぐに動かす。localhost:8181にサーバーが立つ。
code:bash
docker run -p 8181:8080 --init nwtgck/piping-server-rust
Cargo
ソースから動かす方法。--releaseは最適化目的。
code:bash
cargo run --release
オリジナルPiping Serverとの違い
基本的には同じ。パスを指定して、POST/PUTで送信、GETで受信。
ただ、多少の違いがある。
Rust (Hyper) 版とTypeScript版のPiping Serverの違い にまとめている。
技術的な話
HyperをHTTPのサーバーのライブラリとして使っている
調べた感じだと、HyperはRustのデファクトスタンダードなHTTPサーバーライブラリの様子
HTTP/2に対応している
unsafeは使っていない
追記
最新版だとRustのHyperでBodyの終わりを検出して好きな処理をするを使ってものに書き換えていて、以下の内容と多少実装は変化している。
RustのHyperで(req, res)=>voidなハンドラーを使いたいも使っている
Hyperを使わずに低レベルのHTTPサーバーの実装として有名なtiny-httpを使ったものが以下
Rust (tiny-http)でのPiping Server実装
レスポンスを待たせたり、ストリーミングすることへのこだわり
Piping Serverは割とシンプルな仕組みで動いている。だが、HTTPサーバーがよく使われるREST APIサーバーや静的なホスティングとも異なっている。特に送信者がいなければ待ったり、受信者がいなければ待ったり、送信者のHTTPボディを受信者に流し込んだりする。こういう面がよくあるHTTPサーバーの使われ方と異なっている。そのため、Piping Serverで必要な技術をRustとHyperで実現するためにいくつかプロトタイプを作りながら試していった。
「送信者がいなければ待ったり、受信者がいなければ待ったり」の部分は、チャンネルを使っている。具体的にはfutures::sync::oneshot::Receiver<>を使っている。現在のHyperの0.12だと、service_fn()の引数は、Future<Item=Response<Body>...のようなものを返す、以下のコードのようにReceiverがFutureをimplしているため、ちょうどfutures::sync::oneshot::Receiver<Response<Body>>をservice_fn()の引数に渡る関数の戻り値にすることで待たせることが可能になった。
code:rs
// futures-0.1.27/src/sync/oneshot.rs
impl<T> Future for Receiver<T> {
type Item = T;
type Error = Canceled;
fn poll(&mut self) -> Poll<T, Canceled> {
self.inner.recv()
}
}
このチャンネルの使い方は、JavaScriptとかのPromiseをresolveする感覚でReceiverに対応するSenderからBodyを送りこむイメージで使っている。
ハンドラーのAPI - req => res VS (req, res) => void
ハンドラーのAPIとしてレスポンスを戻り値として返すのは、低いレベルでHTTPサーバーを書くときは使いづらいのかなって思っている。つまり、service_fn()の引数が細かいことを無視するとreq: Request<Body> -> Future<Response<Body>>になっているのが扱いづらいと思っている。
Node.jsやGo言語のHTTPサーバーを書くときは、ハンドラーは(req, res) => voidみたいな型になる。イメージ的にはレスポンスは返すものであるが、リクエストとレスポンス同時に操作しやすい利点がある。あと元々ソケットが双方向通信可能な存在であるのでreq, resがハンドラー内で相互に操作できる方が低いレベルのHTTPサーバーが書きやすいのかなと思っている
Hyperも0.10だとreq, resを引数にとったAPIになってい様子。詳しくは:RustのHyperでNode.jsライクなハンドラーは0.10だと使えていた様子
新しいものだとRustのHyperで(req, res)=>voidなハンドラーを使いたいの方法を使っている。
Rust版の今後
現在のところオリジナルを置き換えるつもりなどはなく、TypeScriptのオリジナルと並行して存在する感じ。
まだHyperも1.xでもないし、変化もありそう。少なくともクライアント接続切れの検知ができる実装などができるようにしたい。この高速化はすごく有益なので、リバースプロキシで、例えばppng.ml/fast/mypath1とかppng.ml/fast/mydataだとRust版を使うようにするなどの実験的な試みはしても良いと思っている。